﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using Sonce.Editor.Tools;

namespace Sonce.Editor
{
	/// <summary>
	/// This class handles object selecting and grouping.
	/// </summary>
	public partial class SelectionManager
	{
		#region Members
		private List<FrameworkElement> selectedElements = null;
		#endregion

		#region Properties
		/// <summary>
		/// Gets or sets a parent editor.
		/// </summary>
		public Editor Editor
		{
			get;
			set;
		}

		/// <summary>
		/// Gets a list of selected elements.
		/// </summary>
		public List<FrameworkElement> SelectedElements
		{
			get
			{
				if (this.selectedElements == null)
				{
					this.selectedElements = new List<FrameworkElement>();
				}
				return this.selectedElements;
			}
		}
		#endregion

		#region Constructors
		/// <summary>
		/// Initializes a new SelectionManager instance.
		/// </summary>
		/// <param name="editor"></param>
		public SelectionManager(Editor editor)
		{
			this.Editor = editor;
		}
		#endregion

		#region Selection
		/// <summary>
		/// Find and select element(s) at the specified position.
		/// </summary>
		/// <param name="position">Mouse click position or center point.</param>
		/// <param name="tolerance">Tolerance in pixels.</param>
		public void Select(Point position, double tolerance)
		{
			Rect region = Helper.GetRegion(position, tolerance);
			Select(region, true);
		}

		/// <summary>
		/// Overloaded. Show selection for the specified elements.
		/// </summary>
		public void Select(FrameworkElement element)
		{
			Select(element, true, true);
		}

		/// <summary>
		/// Show selection for the specified elements.
		/// </summary>
		/// <param name="element">Element</param>
		/// <param name="showNodes">Show nodes at the path points or corners.</param>
		/// <param name="showHighlight">Show highlighted lines or whole object.</param>
		public void Select(FrameworkElement element, bool highlightNodes, bool highlightLines)
		{
			//TODO: highlightNodes, highlightLines

			if (!this.SelectedElements.Contains(element))
			{
				Highlight(this.Editor.LayerSelect, element);
				this.SelectedElements.Add(element);
			}
		}

		/// <summary>
		/// Select objects in the region.
		/// </summary>
		/// <param name="layer"></param>
		/// <param name="region"></param>
		public List<FrameworkElement> Select(Rect region, bool intersect)
		{
			List<FrameworkElement> list = FindElementsAt(region, intersect);
			foreach (FrameworkElement element in list)
			{
				if (!this.SelectedElements.Contains(element))
				{
					Select(element);
				}
			}
			return list;
		}

		/// <summary>
		/// Select all available elements on the working area.
		/// </summary>
		public void SelectAll()
		{
			foreach (FrameworkElement element in this.Editor.LayerWorking.Children)
			{
				if (!this.SelectedElements.Contains(element))
				{
					this.SelectedElements.Add(element);
				}
			}

			this.Editor.LayerSelect.Clear();
			Highlight(this.Editor.LayerSelect, this.SelectedElements);
		}

		public void Unselect(Point position, double tolerance)
		{
			Rect region = Helper.GetRegion(position, tolerance);
			Unselect(region, true);
		}

		public void Unselect(Rect region, bool intersect)
		{
			List<FrameworkElement> list = FindElementsAt(region, intersect);
			foreach (FrameworkElement element in list)
			{
				if (this.SelectedElements.Contains(element))
				{
					Unselect(element);
				}
			}
		}

		/// <summary>
		/// Clear the selection for the specified element.
		/// </summary>
		public void Unselect(FrameworkElement element)
		{
			//Clear all and redraw without the specified element
			if (this.SelectedElements.Contains(element))
			{
				this.SelectedElements.Remove(element);
				Highlight(this.Editor.LayerSelect, this.SelectedElements);
			}
		}

		/// <summary>
		/// Clear the selection for the specified element.
		/// </summary>
		public void Unselect(List<FrameworkElement> elements)
		{
			//Clear all and redraw without the specified elements
			foreach (FrameworkElement element in elements)
			{
				if (this.SelectedElements.Contains(element))
				{
					this.SelectedElements.Remove(element);
				}
			}
			Highlight(this.Editor.LayerSelect, this.SelectedElements);
		}

		/// <summary>
		/// Clear the selection layer.
		/// </summary>
		public void UnselectAll()
		{
			this.SelectedElements.Clear();
			this.Editor.LayerSelect.Children.Clear();
			this.Editor.LayerNodes.Children.Clear();
		}

		public void ToggleSelect(Point position, double tolerance)
		{
			Rect region = Helper.GetRegion(position, tolerance);
			ToggleSelect(region, true);
		}

		public void ToggleSelect(Rect region, bool intersect)
		{
			List<FrameworkElement> list = FindElementsAt(region, intersect);
			foreach (FrameworkElement element in list)
			{
				ToggleSelect(element);
			}
		}

		public void ToggleSelect(FrameworkElement element)
		{
			if (this.Editor.SelectedElements.Contains(element))
			{
				Unselect(element);
			}
			else
			{
				Select(element);
			}
		}

		public void ToggleSelect(List<FrameworkElement> elements)
		{
			//TODO: Improve algorithm - handle SelectedElements list then call Highlight once at the end
			foreach (FrameworkElement element in elements)
			{
				if (this.Editor.SelectedElements.Contains(element))
				{
					Unselect(element);
				}
				else
				{
					Select(element);
				}
			}
		}
		#endregion

		#region Grouping
		/// <summary>
		/// Group selected elements to a panel.
		/// </summary>
		/// <returns>Returns group as Canvas.</returns>
		public Canvas Group()
		{
			return Group(this.SelectedElements);
		}

		/// <summary>
		/// Group the specified elements to a panel. Panel is automatically added to the layer.
		/// </summary>
		/// <param name="elements">List of FrameworkElements.</param>
		/// <returns>Returns group as Canvas.</returns>
		public Canvas Group(List<FrameworkElement> elements)
		{
			//TODO: Set layer name
			//TODO: Set canvas size and location
			Canvas group = new Canvas();
			foreach (FrameworkElement element in elements)
			{
				if (element.Parent != null && element.Parent is Panel)
				{
					Panel parent = element.Parent as Panel;
					parent.Children.Remove(element);
				}
				//Unselect(element); //Exception, cannot change the elements array
				group.Children.Add(element);
			}
			this.Editor.LayerWorking.Children.Add(group);
			//Select(group);
			return group;
		}

		public List<FrameworkElement> Ungroup()
		{
			return Ungroup(this.SelectedElements);
		}

		public List<FrameworkElement> Ungroup(List<FrameworkElement> elements)
		{
			List<FrameworkElement> list = new List<FrameworkElement>();
			foreach (FrameworkElement element in elements)
			{
				if (element is Panel)
				{
					list.AddRange(Ungroup((Panel)element));
				}
				else
				{
					list.Add(element);
				}
			}
			return list;
		}

		public List<FrameworkElement> Ungroup(Panel group)
		{
			//TODO: Set layer name
			//TODO: Set canvas size and location
			List<FrameworkElement> list = new List<FrameworkElement>();
			if (group != null && group.Parent != null && group.Parent is Panel)
			{
				Panel parent = group.Parent as Panel;
				while (group.Children.Count > 0)
				{
					FrameworkElement element = (FrameworkElement)group.Children[0];
					group.Children.Remove(element);
					parent.Children.Add(element);
				}
			}

			this.Editor.LayerWorking.Children.Add(group);
			//Select(list);
			return list;
		}
		#endregion

		#region Helpers
		/// <summary>
		/// Overloaded. Check if selected region contains the specified element.
		/// </summary>
		/// <param name="element">Element</param>
		/// <param name="coordinate">Coordinate</param>
		/// <param name="hitTestWidth">Tolerance in pixels.</param>
		public bool Contains(FrameworkElement element, Point coordinate, double hitTestWidth)
		{
			return Contains(element, Helper.GetRegion(coordinate, hitTestWidth));
		}

		/// <summary>
		/// Check if selected region contains the specified element.
		/// </summary>
		/// <param name="element">Element</param>
		/// <param name="selection">Region</param>
		/// <returns>Returns true only if whole element is in the region.</returns>
		public bool Contains(FrameworkElement element, Rect selection)
		{
			bool contains = false;
			if (element != null)
			{
				if (element is Path)
				{
					#region Polyline
					Path polyline = element as Path;
					if (polyline != null && polyline.Data is PathGeometry)
					{
						PathGeometry geometry = polyline.Data as PathGeometry;
						foreach (PathFigure figure in geometry.Figures)
						{
							if (!selection.Contains(figure.StartPoint))
							{
								return false;
							}
							else
							{
								foreach (PathSegment segment in figure.Segments)
								{
									if (segment is LineSegment)
									{
										LineSegment line = segment as LineSegment;
										if (!selection.Contains(line.Point))
										{
											return false;
										}
									}
								}
								contains = true;
							}
						}
					}
					#endregion
				}
				else
				{
					#region Any object
					double x0 = 0;
					double y0 = 0;
					if (element.Width.CompareTo(double.NaN) == 0 && element.Height.CompareTo(double.NaN) == 0 && element is Canvas)
					{
						x0 = element.Margin.Left;
						y0 = element.Margin.Top;
						element = (FrameworkElement)((Canvas)element).Children[0];
					}

					double x1 = element.Margin.Left + x0;
					double y1 = element.Margin.Top + y0;
					double x2 = x1 + element.ActualWidth;
					double y2 = y1 + element.ActualHeight;
					if (selection.Left < x1 && x2 < selection.Right && selection.Top < y1 && y2 < selection.Bottom)
					{
						contains = true;
					}
					#endregion
				}
			}
			return contains;
		}

		/// <summary>
		/// Overloaded. Check if selected region intersects with the specified element.
		/// </summary>
		/// <param name="element">Element</param>
		/// <param name="coordinate">Coordinate</param>
		/// <param name="hitTestWidth">Tolerance in pixels.</param>
		/// <returns></returns>
		public bool Intersect(FrameworkElement element, Point coordinate, double hitTestWidth)
		{
			return Intersect(element, Helper.GetRegion(coordinate, hitTestWidth));
		}

		/// <summary>
		/// Check if selected region intersects with the specified element.
		/// </summary>
		/// <param name="element">Element</param>
		/// <param name="selection">Region</param>
		/// <returns>Returns true if part of element or whole element is in the region.</returns>
		public bool Intersect(FrameworkElement element, Rect selection)
		{
			bool intersect = false;
			if (element != null)
			{
				if (element is Path)
				{
					#region Polyline
					Path polyline = element as Path;
					if (polyline != null && polyline.Data is PathGeometry)
					{
						PathGeometry geometry = polyline.Data as PathGeometry;
						foreach (PathFigure figure in geometry.Figures)
						{
							if (selection.Contains(figure.StartPoint))
							{
								intersect = true;
								return true;
								break;
							}
							else
							{
								List<Point> points = new List<Point>();
								points.Add(figure.StartPoint);
								foreach (PathSegment segment in figure.Segments)
								{
									if (segment is LineSegment)
									{
										LineSegment line = segment as LineSegment;
										if (selection.Contains(line.Point))
										{
											intersect = true;
											return true;
											break;
										}
										points.Add(line.Point);
									}
								}
								if (figure.IsClosed)
								{
									points.Add(figure.StartPoint);
								}
								if (Helper.Intersect(points.ToArray(), selection))
								{
									return true;
								}
							}
						}
					}
					#endregion
				}
				else if (element.Tag is ElementTag)
				{
					ElementTag elementTag = element.Tag as ElementTag;
					foreach (NodeTag nodeTag in elementTag.Nodes)
					{
						if (selection.Contains(nodeTag.Position))
						{
							return true;
							break;
						}
					}
				}
				else
				{
					#region Any object
					double x0 = 0;
					double y0 = 0;
					if (element.Width.CompareTo(double.NaN) == 0 && element.Height.CompareTo(double.NaN) == 0 && element is Canvas)
					{
						x0 = element.Margin.Left;
						y0 = element.Margin.Top;
						element = (FrameworkElement)((Canvas)element).Children[0];
					}

					Rect bounds;
					double x1 = element.Margin.Left + x0;
					double y1 = element.Margin.Top + y0;
					double x2 = x1 + element.ActualWidth;
					double y2 = y1 + element.ActualHeight;
					bounds = new Rect(new Point(x1, y1), new Point(x2, y2));
					bounds.Intersect(selection);
					if (bounds.Width > 0 || bounds.Height > 0)
					{
						intersect = true;
					}
					#endregion
				}
			}
			return intersect;
		}

		public List<FrameworkElement> FindElementsAt(Point point, double tolerance, bool intersect)
		{
			return FindElementsAt(Helper.GetRegion(point, tolerance), intersect);
		}

		public List<FrameworkElement> FindElementsAt(Rect region, bool intersect)
		{
			List<FrameworkElement> selectedElements = new List<FrameworkElement>();
			foreach (FrameworkElement element in this.Editor.LayerWorking.Children)
			{
				bool included = false;
				if (intersect)
				{
					included = Intersect(element, region);
				}
				else
				{
					included = Contains(element, region);
				}

				if (included)
				{
					selectedElements.Add(element);
				}
			}
			return selectedElements;
		}
		#endregion

		#region Highlighting
		//e.g. when dragging a node over a line, highlight line
		public void HighlightTouchingLines(Point position)
		{
			//Find lines, connections
			List<FrameworkElement> links = LogicManager.FindObjects<LinkTag>(this.Editor.LayerWorking);

			//Radial tolerance in pixels
			int tolerance = 2;

			//Clear hovering layer
			this.Editor.LayerFloat.Clear();

			//Highlight all touching lines
			for (int i = 0; i < links.Count; i++)
			{
				List<Point> linkPoints = Helper.GetPathPoints(links[i]); //Get all points of polyline
				if (Helper.IsPointOnLine(linkPoints.ToArray(), position, tolerance))
				{
					Highlight(this.Editor.LayerHighlight, links[i]);
				}
			}
		}

		public List<FrameworkElement> HighlightElementsAt(Layer highlightingLayer, Point point, double tolerance, bool intersect)
		{
			return HighlightElementsAt(highlightingLayer, Helper.GetRegion(point, tolerance), intersect);
		}

		public List<FrameworkElement> HighlightElementsAt(Layer highlightingLayer, Rect region, bool intersect)
		{
			List<FrameworkElement> selectedElements = FindElementsAt(region, intersect);
			bool isFloating = highlightingLayer.IsFloating;
			highlightingLayer.IsFloating = false; //Temporary, do not clear while highlighting elements
			foreach (FrameworkElement element in selectedElements)
			{
				Highlight(highlightingLayer, element);
			}
			highlightingLayer.IsFloating = isFloating;
			return selectedElements;
		}

		//Deep clone on other layer and apply color
		public void Highlight(Layer highlightingLayer, FrameworkElement element)
		{
			if (element != null)
			{
				if (element is Path)
				{
					#region Polyline
					//Show node at the each line segment point
					Path polyline = element as Path;
					if (polyline != null && polyline.Data is PathGeometry)
					{
						PathGeometry geometry = polyline.Data as PathGeometry;
						foreach (PathFigure figure in geometry.Figures)
						{
							LineTool tool = new LineTool() { Editor = this.Editor };
							tool.StartPolyLine(highlightingLayer, figure.StartPoint);
							foreach (PathSegment segment in figure.Segments)
							{
								if (segment is LineSegment)
								{
									LineSegment line = segment as LineSegment;
									tool.ContinuePolyLine(highlightingLayer, line.Point);
								}
							}
							tool.ClosePolyLine(highlightingLayer, figure.IsClosed);
						}
					}
					#endregion
				}
				//else if (element.Tag is GroupTag)
				//{
				//    //TODO: Highlight bounds
				//}
				//else if (element.Tag is ElementTag)
				//{
				//    //TODO: Highlight lines
				//}
				else
				{
					#region Any object
					double x0 = 0;
					double y0 = 0;

					if (double.IsNaN(element.Width) && double.IsNaN(element.Height) && element is Panel)
					{
						x0 = element.Margin.Left;
						y0 = element.Margin.Top;
						element = (FrameworkElement)((Panel)element).Children[0];
					}

					LineTool tool = new LineTool() { Editor = this.Editor };
					if (element is Rectangle)
					{
						Rectangle rectangle = element as Rectangle;
						tool.StartPolyLine(highlightingLayer, new Point(rectangle.Margin.Left, rectangle.Margin.Top));
						tool.ContinuePolyLine(highlightingLayer, new Point(rectangle.Margin.Left + rectangle.Width, rectangle.Margin.Top));
						tool.ContinuePolyLine(highlightingLayer, new Point(rectangle.Margin.Left + rectangle.Width, rectangle.Margin.Top + rectangle.Height));
						tool.ContinuePolyLine(highlightingLayer, new Point(rectangle.Margin.Left, rectangle.Margin.Top + rectangle.Height));
						tool.ContinuePolyLine(highlightingLayer, new Point(rectangle.Margin.Left, rectangle.Margin.Top));
					}
					else
					{
						Rect bounds = LayoutManager.GetAbsoluteBounds(element);
						tool.StartPolyLine(highlightingLayer, new Point(bounds.Left, bounds.Top));
						tool.ContinuePolyLine(highlightingLayer, new Point(bounds.Left + bounds.Width, bounds.Top));
						tool.ContinuePolyLine(highlightingLayer, new Point(bounds.Left + bounds.Width, bounds.Top + bounds.Height));
						tool.ContinuePolyLine(highlightingLayer, new Point(bounds.Left, bounds.Top + bounds.Height));
						tool.ContinuePolyLine(highlightingLayer, new Point(bounds.Left, bounds.Top));
					}
					#endregion
				}
			}
		}

		public void Highlight(Layer highlightingLayer, List<FrameworkElement> elements)
		{
			foreach (FrameworkElement element in elements)
			{
				Highlight(highlightingLayer, element);
			}
		}
		#endregion
	}
}
